#!/usr/bin/env node
//参考： https://hbase.js.org/learn/developer
const os= require('os');
const hbase = require('hbase');
const fs = require('fs-extra');
const path = require('path');
const moment = require('moment');
const command = require('./js/command');
const utils = require('./js/utils');
const _ = require('lodash');
const program = require('commander');
const shell = require('shelljs');
const configFile = path.join(os.homedir(), '.hbase_config');
const CACHE = {};
let _host = 'localhost';
let _port = 8080;

let _verbose = true;
let _prettyShell = true;
let _highLightStyle;
let _limit = 1;
let _maxLimit = 1000;
async function createHBase (commands, options) {
    const db = hbase({ host: _host, port: _port });
    const tables = await getTables(db);
    for (const table of tables) {
        await getFieldsOfTable(db, table);
    }
    // 添加表，字段的自动完成
    async function completer(line, callback) {
        const words = line.replace(/\s,\s/g, ',').trimLeft().split(/\s/);
        const linePartial = _.last(_.last(words).split(','));
        let list = [];
        if (words.length <= 1) {
            const cmds = [ 'scan', 'get', 'put', 'delete', 'deleteall' ];
            const _linePartial = line.trim();
            list = cmds.filter((o) => o.startsWith(_linePartial));
            return callback(null, [list, _linePartial]);
        }

        const preWord = _.toLower(_.nth(words, -2));
        if (_.includes(['scan', 'get', 'put', 'delete', 'deleteall', 'ls', 's', 'm', 'r', 'c'], preWord)) {
            list = CACHE._tables.filter((o) => o.startsWith(linePartial));
        }  else {
            const prePreWord = _.toLower(_.nth(words, -3));
            if (_.includes(['s', 'm', 'r', 'c'], prePreWord)) {
                const table = preWord;
                const fields = CACHE[table];
                list = fields.filter((o) => o.startsWith(linePartial));
            } else {
                const prePrePreWord = _.toLower(_.nth(words, -4));
                if (_.includes(['s', 'm', 'c'], prePrePreWord)) {
                    const table = prePreWord;
                    const fields = CACHE[table];
                    list = fields.filter((o) => o.startsWith(linePartial));
                } else {
                    let allFileds = [];
                    for (const table of CACHE._tables) {
                        const fields = CACHE[table];
                        allFileds = [...allFileds, ...fields];
                    }
                    allFileds = _.uniq(allFileds);
                    list = allFileds.filter((o) => o.startsWith(linePartial));
                }
            }
        }
        const found = list.filter((o) => o.startsWith(linePartial));
        return callback(null, [found.length ? found : list, linePartial]);
    }
    CACHE._options = { ...options, db, completer };
    command(commands, CACHE._options);
}
function base64(data) {
    return Buffer.from(data, 'utf8').toString('base64');
}
function dropBrack(line) {
    while (_.first(line) === '(' && _.last(line) === ')') {
        line = line.substr(1, line.length-2);
    }
    return line;
}

function parseItem(item) {
    const list = item.match(/([\w\.]+)(!=|>=|>|<=|<|=|~|#)(.*)/);
    if (!list) {
        return null;
    }
    let key = list[1];
    let oper = list[2];
    let value = list[3];
    if (key === 'id') {
        return { id: value };
    }

    if (!/\./.test(key) || oper === undefined || value === undefined) {
        return null;
    }
    let op = 'EQUAL';
    let compType = 'BinaryComparator';
    switch (oper) {
        case '=': op = 'EQUAL'; break;
        case '!=': op = 'NOT_EQUAL'; break;
        case '!~': op = 'NOT_EQUAL';
        case '~': value = (''+value).replace(/</, '^').replace(/>/, '$'); compType = 'RegexStringComparator'; break;
    }
    const keys = key.split('.');
    return {
        op,
        latestVersion: true,
        ifMissing: op === 'EQUAL',
        type: "SingleColumnValueFilter",
        family: base64(keys[0]),
        qualifier: base64(keys[1]),
        comparator: { value, type: compType },
    };
}
function parseAnd(line) {
    if (!/\||&/.test(line)) {
        return parseItem(line);
    }
    const list = [];
    let i = -1, max = line.length - 1, item = '', match = 0;
    while (i++ < max) {
        const ch = line[i];
        if (ch === '(') {
            match++;
        } else if (ch === ')') {
            match--;
        }
        if (ch === '&' && match === 0) {
            list.push(parseAnd(dropBrack(item)));
            item='';
        } else {
            item=`${item}${ch}`;
        }
    }
    if (match === 0) { // 如果括号不匹配，则丢弃
        if (line === item) {
            item && list.push(parseOr(dropBrack(item)));
        } else {
            item && list.push(parseAnd(dropBrack(item)));
        }
    }
    return list.length > 1 ? { op: 'MUST_PASS_ALL', type: 'FilterList', filters: list } : list[0];
}
function parseOr(line) {
    if (!/\||&/.test(line)) {
        return parseItem(line);
    }
    const list = [];
    let i = -1, max = line.length - 1, item = '', match = 0;
    while (i++ < max) {
        const ch = line[i];
        if (ch === '(') {
            match++;
        } else if (ch === ')') {
            match--;
        }
        if (ch === '|' && match === 0) {
            list.push(parseOr(dropBrack(item)));
            item='';
        } else {
            item=`${item}${ch}`;
        }
    }
    if (match === 0) { // 如果括号不匹配，则丢弃
        if (line === item) {
            item && list.push(parseAnd(dropBrack(item)));
        } else {
            item && list.push(parseOr(dropBrack(item)));
        }
    }
    return list.length > 1 ? { op: 'MUST_PASS_ONE', type: 'FilterList', filters: list } : list[0];
}
function formatResult(obj) {
    if (obj instanceof Array) {
        return obj.map(o=>formatResult(o));
    }
    for (const key in obj) {
        const item = obj[key];
        if (item instanceof Array) {
            obj[key] = formatResult(item);
        } else if (item instanceof Date) {
            obj[key] = moment(item).format('YYYY-MM-DD HH:mm:ss.SSS');
        }
    }
    return obj;
}
function formatFilter(condLine, limit) {
    const limitFilter = { type:"PageFilter", value: limit };

    if (condLine) {
        if (/^[0-9a-z]{24}$/.test(condLine)) {
            condLine = `id=${condLine}`;
        } else if (/^1[0-9]{10}$/.test(condLine)) {
            condLine = `phone=${condLine}`;
        } else if (/^[0-9]{17}[0-9xX]$/.test(condLine)) {
            condLine = `idNo=${condLine}`;
        }
        const cond = parseOr(dropBrack(condLine));
        if (cond) {
            if (cond.id) {
                return { startRow: cond.id, filter: limitFilter };
            }
            return { filter: { op: 'MUST_PASS_ALL', type: 'FilterList', filters: [ cond, limitFilter ] } };
        }
    }
    return {};
}
function formatSetFields(line) {
    const list = [];
    let flag = true;
    while (flag) {
        let matches = line.match(/^([a-zA-Z][a-zA-Z0-9_]*\.[a-zA-Z][a-zA-Z0-9_]*):(.*)/);
        if (matches) {
            const key = matches[1];
            let value = matches[2];
            if (/^[^;]*=>{/.test(value)) { //带{函数
                let i = -1, max = value.length, match = 0;
                while (i++ < max) {
                    const ch = value[i];
                    if (ch === '{') {
                        match++;
                    } else if (ch === '}') {
                        match--;
                    }
                    if ((ch === ';' || !ch) && match === 0) {
                        line = value.substr(i+1);
                        value = value.substr(0, i);
                        list.push({ key, value });
                        if (!line) {
                            flag = false;
                        }
                        break;
                    }
                }
                if (match !== 0) {
                    return null;
                }
            } else if (_.startsWith(value, '[')) { // 数组
                let i = -1, max = value.length, bmatch = 0, cmatch = 0;
                while (i++ < max) {
                    const ch = value[i];
                    if (ch === '[') {
                        bmatch++;
                    } else if (ch === ']') {
                        bmatch--;
                    } else if (ch === '{') {
                        cmatch++;
                    } else if (ch === '}') {
                        cmatch--;
                    }
                    if ((ch === ';' || !ch) && bmatch === 0) {
                        if (cmatch !== 0) {
                            return null;
                        }
                        line = value.substr(i+1);
                        value = value.substr(0, i);
                        list.push({ key, value });
                        if (!line) {
                            flag = false;
                        }
                        break;
                    }
                }
                if (bmatch !== 0) {
                    return null;
                }
            } else if (_.startsWith(value, '{')) { // 对象
                let i = -1, max = value.length, bmatch = 0, cmatch = 0;
                while (i++ < max) {
                    const ch = value[i];
                    if (ch === '[') {
                        bmatch++;
                    } else if (ch === ']') {
                        bmatch--;
                    } else if (ch === '{') {
                        cmatch++;
                    } else if (ch === '}') {
                        cmatch--;
                    }
                    if ((ch === ';' || !ch) && cmatch === 0) {
                        if (bmatch !== 0) {
                            return null;
                        }
                        line = value.substr(i+1);
                        value = value.substr(0, i);
                        list.push({ key, value });
                        if (!line) {
                            flag = false;
                        }
                        break;
                    }
                }
                if (cmatch !== 0) {
                    return null;
                }
            } else {
                matches = value.match(/([^;]*);(.*)/);
                if (!matches) {
                    list.push({ key, value });
                    break;
                }
                value = matches[1];
                list.push({ key, value });
                line = matches[2];
            }
        } else {
            return null;
        }
    }
    return list;
}
function formatUpdate(self, doc, setFields) {
    if (!setFields) {
        return {};
    }
    const obj = {};
    for (const item of setFields) {
        const key = item.key;
        let value = item.value||'';
        const matches = _.uniq(value.match(/\${\w+\.\w+}/g));
        for (const match of matches) {
            const refValue =  _.get(doc, match.replace('${', '').replace('}', ''));
            if (refValue != undefined) {
                value = value.replace(new RegExp(match.replace('$', '\\$'), 'g'), refValue);
            }
        }
        // 解析函数
        if (/^[^;]*=>/.test(value)) {
            try {
                const func = (new Function(`return ${value}`))();
                value = func(doc, {_, utils,moment})+'';
            } catch(e) {
                self.error(`Function:${value} 格式错误`);
                continue;
            }
        }
        if (/\+|-|\*|\//.test(value)) {
            try { value = eval(value)+'' } catch(err) {}
        }
        obj[key] = value;
    }
    return obj;
}
async function formatFields(db, table, line, cond) {
    if (!line || line === '*') {
        return {};
    }
    let include = true;
    if (line[0] === '-') {
        include = false;
        line = line.substr(1);
    }
    let list = line.split(',').filter(Boolean);
    const allFields = await getFieldsOfTable(db, table, true, cond);
    list = list.map(o => new RegExp(o.replace('<', '^').replace('>', '$'), 'i'));

    const fields = {};
    allFields.filter(o=>_.some(list, r=>r.test(o))).forEach(o=>fields[o] = include ? 1 : 0);
    return fields;
}
function formatSort(db, table, line) {
    if (!line) {
        return {};
    }
    const sort = {};
    line = line.split(',');
    for (const i in line) {
        let item = line[i];
        let order = item[item.length-1] === '+' ? 1 : -1;
        item = item.substr(0, item.length-1);
        sort[item] = order;
    }
    return sort;
}
function formatLine(self, line) {
    let i = -1, max = line.length - 1, newLine = '', amatch = 0, bmatch = 0, cmatch = 0;
    while (i++ < max) {
        let ch = line[i];
        if (ch === '[') {
            amatch++;
        } else if (ch === ']') {
            amatch--;
        }
        if (ch === '{') {
            bmatch++;
        } else if (ch === '}') {
            bmatch--;
        }
        if (ch === '(') {
            cmatch++;
        } else if (ch === ')') {
            cmatch--;
        }
        if (amatch < 0 || bmatch < 0 || cmatch < 0) {
            self.error('括号不匹配');
            return [];
        }
        if (!(ch === ' ' && (amatch || bmatch || cmatch))) {
            newLine += ch;
        }
    }
    if (amatch || bmatch || cmatch) {
        self.error('括号不匹配');
        return [];
    }
    return newLine
    .replace(/"[^"]+"/g, o=>o.replace(' ', '&nbsp;'))
    .replace(/'[^']+'/g, o=>o.replace(' ', '&nbsp;'))
    .replace(/\s*:\s*/g, ':')
    .replace(/\s*;\s*/g, ';')
    .replace(/\s*,\s*/g, ',')
    .replace(/\s*!=\s*/g, '!=')
    .replace(/\s*!==\s*/g, '!==')
    .replace(/\s*=\s*/g, '=')
    .replace(/\s*==\s*/g, '==')
    .replace(/\s*~\s*/g, '~')
    .replace(/\s*\|\s*/g, '|')
    .replace(/\s*&\s*/g, '&')
    .replace(/\s*#\s*/g, '#')
    .replace(/\s*=>\s*/g, '=>')
    .trim().split(/\s/).filter(Boolean).map(o=>o.replace('&nbsp;', ' '));
}
function j2s(obj) {
    return JSON.stringify(obj, (k, v)=>{
        if (v instanceof RegExp) {
            return v.toString();
        }
        return v;
    });
}
function getTableNames(db) {
    return new Promise(resolve=>{
        db.tables((err, tables) => {
            if (err) {
                resolve([]);
            } else {
                resolve(tables.map(o=>o.name));
            }
        });
    });
}
function getFields(db, table, cond) {
    return new Promise(resolve=>{
        db.table(table).schema((err, schema) => {
            if (err) {
                resolve([]);
            } else {
                resolve(schema.ColumnSchema.map(o=>o.name));
            }
        });
    });
}
async function getTables(db, force) {
    if (!CACHE._tables || force) {
        CACHE._tables = await getTableNames(db);
    }
    return CACHE._tables;
}
async function getFieldsOfTable(db, table, force, cond) {
    if (!CACHE[table] || force) {
        CACHE[table] = await getFields(db, table, cond);
    }
    return CACHE[table];
}
async function executeLine(line, { db }) {
    if (!/^(scan|put|get|delete|deleteall)\s/.test(line)) {
        return this.prompt();
    }
    const list = line.split(/[\s,]/).filter(Boolean).map(o=>o.replace('.', ':'));
    const cmd = `${list[0]} ${list.slice(1).map(o=>`'${o}'`).join(',')}`;
    if (_verbose) {
        this.verbose(cmd);
    }
    shell.exec(`echo "${cmd}"|hbase shell 2>&1|sed -n '/${list[0]}/,/in.*seconds/p'|sed '1d;$d'`, { silent:true }, (code, stdout, stderr) => {
        this.print(stdout);
        this.prompt();
    });
}
function showJson(self, docs) {
    self.showJson(formatResult(docs), _prettyShell, _highLightStyle);
}
async function showTables(line, { db }) {
    if (!line || line === '.') { // show tables
        this.print('tables:', 'red');
        const tables = await getTables(db, true);
        this.print(tables.join('  '), 'green');
        this.prompt();
    } else { // show table fields
        line = line.replace(/[^\w]*/g, '');
        const fields = await getFieldsOfTable(db, line, true);
        this.print(fields.join('  '), 'green');
        this.prompt();
    }
}
function getLimitIndex(list) {
    return _.findIndex(list, o=>+o==o && o<=_maxLimit);
}
function getCondIndex(list) {
    return _.findIndex(list, o=>/!=|>=|[^=]>\w|=[\w\u4e00-\u9fa5'"]|<=|<\w|~|#/.test(o) || /^[0-9a-z]{24}$/.test(o) || /^1[0-9]{10}$/.test(o) || /^[0-9]{17}[0-9xX]$/.test(o));
}
function getSortIndex(list) {
    return _.findIndex(list, o=>/[a-zA-Z][a-zA-Z0-9_]*(\+|-)$/.test(o));
}
function getSetFieldsIndex(list) {
    return _.findIndex(list, o=>/^[a-zA-Z][a-zA-Z0-9_]*\.[a-zA-Z][a-zA-Z0-9_]*:/.test(o));
}
function matchFields(line, column) {
    if (!line || line === '*') {
        return true;
    }
    let include = true;
    if (line[0] === '-') {
        include = false;
        line = line.substr(1);
    }
    let list = line.split(',').filter(Boolean);
    const names = column.split(':');
    let found = false;
    for (const item of list) {
        if (/\./.test(item)) {
            const items = item.split('.');
            if (items[0] === names[0]) {
                const reg = new RegExp(items[1].replace('<', '^').replace('>', '$'), 'i');
                if (reg.test(names[1])) {
                    found = true;
                }
            }
        } else {
            const reg = new RegExp(item.replace('<', '^').replace('>', '$'), 'i');
            if (reg.test(names[1])) {
                found = true;
            }
        }
    }
    return include ? found : !found;
}
async function showTableRows (line, { db }) {
    if (!line) {
        return this.prompt();
    }
    const matches = line.match(/out=[^\s]*/);
    let writeFile;
    if (matches) {
        writeFile = matches[0].replace('out=', '');
        line = line.replace(matches[0], '');
    }

    let showCount = false;
    let list = formatLine(this, line);
    const table = list[0];
    if (!table) {
        return this.error('invalid oper');
    }
    list = _.drop(list);

    let limit = 0;
    let limitIndex = getLimitIndex(list);
    if (limitIndex !== -1) {
        limit = +list[limitIndex];
        list.splice(limitIndex, 1);
        if (limit < 0) {
            showCount = true;
        }
    } else {
        limit = _limit;
    }

    let condLine = '';
    let condIndex = getCondIndex(list);
    if (condIndex !== -1) {
        condLine = list[condIndex];
        list.splice(condIndex, 1);
    }

    let sort = '';
    let sortIndex = getSortIndex(list);
    if (sortIndex !== -1) {
        sort = formatSort(db, table, list[sortIndex]);
        list.splice(sortIndex, 1);
    }

    const fieldsLine = list[0];
    const filter = formatFilter(condLine, limit);
    if (showCount) {
        if (_verbose) {
            this.verbose(`count ${table}`);
        }
        return db.table(table).scan(filter, (err, rows) => {
            if (err) {
                this.print(err, 'red');
            } else {
                const keys = _.uniq(rows.map(o=>o.key));
                showJson(this, { count: keys.length });
            }
        });
    }
    if (_verbose) {
        this.verbose(`scan '${table}'`);
    }
    db.table(table).scan(filter, (err, rows) => {
        if (err) {
            this.print('no record', 'red');
        } else {
            const keys = _.uniq(rows.map(o=>o.key));
            const ret = {};
            for (const item of rows) {
                const key = item.key;
                if (!matchFields(fieldsLine, item.column)) {
                    continue;
                }
                const names = item.column.split(':');
                if (!ret[key]) {
                    ret[key] = { [names[0]]: { [names[1]]: item['$'] } };
                } else if (!ret[key][names[0]]) {
                    ret[key] = { ...ret[key], [names[0]]: { [names[1]]: item['$'] } };
                } else {
                    ret[key][names[0]] = { ...ret[key][names[0]], [names[1]]: item['$'] };
                }
            }
            showJson(this, ret);
        }
    });
}
function modifyTable(line, { db }) {
    if (!line) {
        return this.prompt();
    }
    let list = formatLine(this, line);
    const table = list[0];
    if (!table) {
        return this.error('invalid oper');
    }
    list = _.drop(list);

    let limit = 0;
    let limitIndex = getLimitIndex(list);
    if (limitIndex !== -1) {
        limit = +list[limitIndex];
        list.splice(limitIndex, 1);
    } else {
        limit = _limit;
    }

    let condLine = '';
    let condIndex = getCondIndex(list);
    if (condIndex !== -1) {
        condLine = list[condIndex];
        list.splice(condIndex, 1);
    }

    let sort = '';
    let sortIndex = getSortIndex(list);
    if (sortIndex !== -1) {
        sort = formatSort(db, table, list[sortIndex]);
        list.splice(sortIndex, 1);
    }

    let setFieldsIndex = getSetFieldsIndex(list);
    if (setFieldsIndex === -1) {
        return this.error('no set fields');
    }
    const setFields = formatSetFields(list[setFieldsIndex]);
    if (!setFields) {
        return this.error('set fields error');
    }
    list.splice(setFieldsIndex, 1);

    if (_verbose) {
        this.verbose(`scan ${table}`);
    }
    const filter = formatFilter(condLine, limit);
    db.table(table).scan(filter, (err, rows) => {
        if (err) {
            this.print('no record', 'red');
        } else {
            const keys = _.uniq(rows.map(o=>o.key));
            const ret = {};
            for (const item of rows) {
                const key = item.key;
                const names = item.column.split(':');
                if (!ret[key]) {
                    ret[key] = { [names[0]]: { [names[1]]: item['$'] } };
                } else if (!ret[key][names[0]]) {
                    ret[key] = { ...ret[key], [names[0]]: { [names[1]]: item['$'] } };
                } else {
                    ret[key][names[0]] = { ...ret[key][names[0]], [names[1]]: item['$'] };
                }
            }
            for (const id in ret) {
                const doc = ret[id];
                const update = formatUpdate(this, doc, setFields);
                for (let keys in update) {
                    const row = db.table(table).row(key);
                    const value = update[keys];
                    const field = keys.replace(/\./, ':');
                    if (value === 'undefined') {
                        row.delete(field, (error, success) => {
                            if (error) {
                                this.error(error);
                            } else {
                                this.success(`delete ${field} success`);
                            }
                        });
                    } else {
                        row.put(field, value, (error, success) => {
                            if (error) {
                                this.error(error);
                            } else {
                                this.success(`set ${field} = ${value} success`);
                            }
                        });
                    }
                }
            }
        }
    });
}
function removeTable(line, { db }) {
    if (!line) {
        return this.prompt();
    }
    let list = formatLine(this, line);
    const table = list[0];
    if (!table) {
        return this.error('invalid oper');
    }
    list = _.drop(list);

    let limit = 0;
    let limitIndex = getLimitIndex(list);
    if (limitIndex !== -1) {
        limit = +list[limitIndex];
        list.splice(limitIndex, 1);
    } else {
        limit = _limit;
    }

    let condLine = '';
    let condIndex = getCondIndex(list);
    if (condIndex !== -1) {
        condLine = list[condIndex];
        list.splice(condIndex, 1);
    }

    let sort = '';
    let sortIndex = getSortIndex(list);
    if (sortIndex !== -1) {
        sort = formatSort(db, table, list[sortIndex]);
        list.splice(sortIndex, 1);
    }

    if (_verbose) {
        this.verbose(`delete`);
    }
    const filter = formatFilter(condLine, limit);
    db.table(table).scan(filter, (err, rows) => {
        if (err) {
            this.print('no record', 'red');
        } else {
            const keys = _.uniq(rows.map(o=>o.key));
            for (const key of keys) {
                db.table(table).row(key).delete((error, success) => {
                    if (error) {
                        this.error(error);
                    } else {
                        this.success(`delete row ${key} success`);
                    }
                });
            }
        }
    });
}
function copyTableRows(line, { db }) {
    if (!line) {
        return this.prompt();
    }
    let list = formatLine(this, line);
    const table = list[0];
    if (!table) {
        return this.error('invalid oper');
    }
    list = _.drop(list);

    let newIdFunc;
    let newIdIndex = _.findIndex(list, o=>/newId=.*/.test(o));
    if (newIdIndex !== -1) {
        if (!/=>/.test(list[newIdIndex])) {
            newIdFunc = list[newIdIndex];
        } else {
            try {
                newIdFunc = (new Function(`return ${list[newIdIndex]}`))();
            } catch(e) {
                newIdFunc = (id)=>`${id}_new`;
            }
        }
        list.splice(newIdIndex, 1);
    }

    let limit = 0;
    let limitIndex = getLimitIndex(list);
    if (limitIndex !== -1) {
        limit = +list[limitIndex];
        list.splice(limitIndex, 1);
    } else {
        limit = _limit;
    }

    let condLine = '';
    let condIndex = getCondIndex(list);
    if (condIndex !== -1) {
        condLine = list[condIndex];
        list.splice(condIndex, 1);
    }

    let sort = '';
    let sortIndex = getSortIndex(list);
    if (sortIndex !== -1) {
        sort = formatSort(db, table, list[sortIndex]);
        list.splice(sortIndex, 1);
    }

    let setFieldsIndex = getSetFieldsIndex(list);
    let setFields = null;
    if (setFieldsIndex !== -1) {
        setFields = list[setFieldsIndex];
        setFields = formatSetFields(setFields);
        if (!setFields) {
            return this.error('set fields error');
        }
        list.splice(setFieldsIndex, 1);
    }

    if (_verbose) {
        this.verbose(`copy`);
    }
    const filter = formatFilter(condLine, limit);
    db.table(table).scan(filter, (err, rows) => {
        if (err) {
            this.print('no record', 'red');
        } else {
            const keys = _.uniq(rows.map(o=>o.key));
            const ret = {};
            for (const item of rows) {
                const key = item.key;
                const names = item.column.split(':');
                if (!ret[key]) {
                    ret[key] = { [names[0]]: { [names[1]]: item['$'] } };
                } else if (!ret[key][names[0]]) {
                    ret[key] = { ...ret[key], [names[0]]: { [names[1]]: item['$'] } };
                } else {
                    ret[key][names[0]] = { ...ret[key][names[0]], [names[1]]: item['$'] };
                }
            }
            for (const id in ret) {
                const doc = ret[id];
                const newId = newIdFunc && (_.isFunction(newIdFunc) ? newIdFunc(_id, doc) : newIdFunc);
                const row = db.table(table).row(newId);
                const update = formatUpdate(this, doc, setFields); //{ fang.name: 123 }
                for (let familyColumn in doc) {
                    const cell = doc[familyColumn];
                    for (let key in cell) {
                        let modifyValue = _.get(update, `${familyColumn}.${key}`);
                        let value = modifyValue === undefined ? cell[key] : modifyValue;
                        row.put(`${familyColumn}:${key}`, value, (error, success) => {
                            if (error) {
                                this.error(error);
                            } else {
                                this.success(`copy ${newId} ${familyColumn}:${key} = ${value} success`);
                            }
                        });
                    }
                }
            }
        }
    });
}
function togglePrettyShell() {
    _prettyShell = !_prettyShell;
    this.print('pretty format change to ' + _prettyShell);
    this.prompt();
}
function toggleVerbose() {
    _verbose = !_verbose;
    this.print('verbose format change to ' + _verbose);
    this.prompt();
}
function setHighLightStyle(value) {
    _highLightStyle = value == 0 ? 'railscasts' : value == 1 ? 'github' : _highLightStyle == 'railscasts' ? 'github' : 'railscasts';
    this.print('highLightStyle set to ' + _highLightStyle);
    this.prompt();
}
function setMaxLimit(line) {
    _maxLimit = +line || 1000;
    this.print('set limit to ' + _maxLimit);
    this.prompt();
}
async function dumpDatabase(line = '') {
    let list = line.split(/\s+/).filter(Boolean);
    let index = list.indexOf('-o');
    let dir;
    if (index !== -1) {
        const items = list.splice(index, 2);
        dir = items[1];
    }
    if (!dir) {
        dir = _database;
    }
    if (fs.existsSync(path.join(dir, _database))) {
        return this.error(`${path.join(dir, _database)} 已经存在`);
    }
    let condition;
    if (_.find(list, o=>/^-/.test(o))) {
        condition = _.map(list, o=>`--excludeCollection ${o.replace(/^-/, '')}`).join(' ');
    } else {
        condition = _.map(list, o=>`--collection ${o}`).join(' ');
    }

    const cmd = `hbasedump -h ${_host} -d ${_database} ${condition} -o ${dir}`;
    this.log(`cmd: ${cmd}`);
    shell.exec(cmd, { silent:true }, (code, stdout, stderr) => {
        if (stderr && /error/.test(stderr)) {
            return this.error(stderr);
        }
        this.success(`目标文件：${process.cwd()}/${dir}`);
    });
}
function showHelp () {
    this.print('commands:', 'blue');
    this.print('    h: show help');
    this.print('    help: show full help');
    this.print('    <q|exit>: exit');
    this.print('    ls: showTables|ls ..: showDatabases|ls table: showTableFields');
    this.print('    s(showTableRows): s table f1,f2>,<f3> (f1=xx&f2=yy)|f3=zz|f4~<xx>|f5#1,2,3 limit(-1:count) sortField[+-] [out=file] [r] [f1:$f1+1]');
    this.print('    m(modifyTable): m table f1:$f1+1;f2:xx;f3:(doc, {_, utils, moment})=>xx $cond limit sortField[+-]');
    this.print('    r(removeTable): # r table $cond limit sortField[+-]');
    this.print('    c(copyTableRows): c table $update $cond limit sortField[+-]');
    this.print('    format: [undefined: $unset][id: \"xx\"|\'xx\':string xx:ObjectId][date:\"xx\":string xx|\'xx\':date][number:\"xx\"|\'xx\':string xx:number]');
    this.print('    _: togglePrettyShell');
    this.print('    v: toggleVerbose');
    this.print('    hl: setHighLightStyle 0: railscasts 1: github else toggle');
    this.print('    limit: setMaxLimit');
    this.print('');
    this.prompt();
}
const COMMANDS = {
    'h': showHelp,
    'help': showHelp,
    'q|exit': function () { this.close() },
    'cls': function() { this.clear() },
    'ls': showTables,
    's': showTableRows,
    'm': modifyTable,
    'r': removeTable,
    'c': copyTableRows,
    '_': togglePrettyShell,
    'v': toggleVerbose,
    'hl': setHighLightStyle,
    'limit': setMaxLimit,
    'default': executeLine,
};


program
.version('1.0.0')
.option('-t, --host [host]', 'set host', '')
.option('-p, --port [port]', 'set port', '')
.option('-c, --config', 'show config', '')
.parse(process.argv);

let options = {};
try {
    options = fs.readJSONSync(configFile);
} catch (e) {}
_host = program.host || options.host || _host;
_port = program.port || options.port || _port;
if (options.host !== _host || options.port !== _port) {
    fs.writeJSONSync(configFile, {  host: _host, port: _port });
}
if (program.config) {
    console.log({ host: _host, port: _port });
    process.exit(0);
}
createHBase(COMMANDS, { title: 'hbase', history: [ 's', 'm', 'r', 'c' ] });
